#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/12/11 21:04
# @Author  : 唐松
# @File    : assertion_util.py
# @Software: PyCharm

import operator
from typing import Callable, Any
import allure
import jsonpath
from util_tools.db_connector.connectMysql import ConnectMysql
from util_tools.logger_util import logs


class AssertionsUtils:
    """
    接口断言模式封装
    1） 状态码断言
    2） 包含模式断言
    3） 相等断言
    4） 不相等断言
    5） 数据库断言
    """

    @classmethod
    def status_code_assert(cls, expected_result, status_code):
        """
        接口的响应状态码断言
        :param expected_result: （int）yaml文件code模式中的预期状态码
        :param status_code: （int）接口实际返回的状态码
        :return:
        """
        # # 初始化失败计数器为0,断言状态标识, 0 表示成功, 其他表示失败
        failure_count = 0
        # 检查预期结果是否为整数, # 如果不是, 将其转换为整数
        if not isinstance(expected_result, int):
            expected_result = int(expected_result)
        # 比较预期状态码与实际状态码
        if expected_result == status_code:
            logs.info(f'状态码断言成功: 接口实际返回状态码 【{status_code}】 == 【{expected_result}】')
            allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(status_code)}", "状态码断言结果: 成功",
                          attachment_type=allure.attachment_type.TEXT)
        else:
            logs.error(f'状态码断言失败: 接口实际返回状态码 【{status_code}】 != 【{expected_result}】')
            failure_count += 1
            allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(status_code)}", "状态码断言结果: 失败",
                          attachment_type=allure.attachment_type.TEXT)

        return failure_count

    @classmethod
    def contain_assert(cls, expected_result, response):
        """
        字符串包含模式, 断言预期结果字符串是否包含在接口的实际响应返回信息中
        :param expected_result: （dict或str）yaml文件里面contain模式的数据
        :param response: （dict）接口的实际响应信息
        :return:
        """
        # # 初始化失败计数器为0,断言状态标识, 0 表示成功, 其他表示失败
        failure_count = 0

        # 如果预期结果是字符串, 则直接在response字符串中查找
        if isinstance(expected_result, str):
            # 检查预期字符串是否在响应中
            if expected_result in str(response):
                logs.info(f"包含模式断言成功: 预期结果【{expected_result}】存在于实际结果【{response}】中")
                allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(response)}", "包含模式断言结果: 成功",
                              attachment_type=allure.attachment_type.JSON)
            else:
                # 如果不包含, 增加失败计数器
                failure_count += 1
                logs.error(f"包含模式断言失败: 预期结果【{expected_result}】未在实际结果【{response}】中找到")
                allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(response)}", "包含模式断言结果: 失败",
                              attachment_type=allure.attachment_type.JSON)
        # 如果预期结果是字典
        elif isinstance(expected_result, dict):
            # 遍历字典中的每个键值对
            for assert_key, assert_value in expected_result.items():
                # 使用jsonpath查找响应中的键
                response_list = jsonpath.jsonpath(response, f'$..{assert_key}')
                # 如果找到, 并且是字符串,将列表中的字符串连接成一个字符串
                if response_list and all(isinstance(item, str) for item in response_list):
                    # 将列表中的字符串连接成一个字符串
                    response_str = ''.join(response_list)
                    # 检查预期值是否在响应字符串中
                    if assert_value in response_str:
                        logs.info(f"包含模式断言成功: 预期结果【{assert_value}】存在于实际结果【{response_str}】中")
                        allure.attach(f"预期结果: {str(assert_value)}\n实际结果: {str(response_str)}",
                                      "包含模式断言结果: 成功",
                                      attachment_type=allure.attachment_type.JSON)
                    else:
                        # 如果不包含, 增加失败计数器
                        failure_count += 1
                        logs.error(f"包含模式断言失败: 预期结果【{assert_value}】未在实际结果【{response_str}】中找到")
                        allure.attach(f"预期结果: {str(assert_value)}\n实际结果: {str(response_str)}",
                                      "包含模式断言结果: 失败",
                                      attachment_type=allure.attachment_type.JSON)
                else:
                    # 如果找不到或者不是字符串, 增加失败计数器
                    failure_count += 1
                    logs.error(f"包含模式断言失败: 无法在实际结果中找到预期的键【{assert_key}】")

        else:
            # 如果既不是字典也不是字符串, 则抛出异常
            raise ValueError("expected_result参数必须是dict或str类型")
        return failure_count

    @classmethod
    def equal_assert(cls, expected_result, response):
        """
        相等断言, 根据yaml里面的validation关键词下面的eq模式数据去跟接口实际响应信息对比
        :param expected_result: （dict）yaml里面的eq值
        :param response: （dict）接口实际响应结果
        :return:
        """
        failure_count = 0
        # 检查响应和预期结果是否都是字典
        if isinstance(response, dict) and isinstance(expected_result, dict):
            # 找出实际结果与预期结果共同的key值
            common_key = list(expected_result.keys() & response.keys())
            if common_key:
                # 取出第一个共同的键
                common_key = common_key[0]
                # 根据相同的key值去实际结果中获取, 并重新生成一个实际结果的字典
                new_actual_result = {common_key: response[common_key]}
                # 使用operator.eq检查相等性
                eq_assert = operator.eq(new_actual_result, expected_result)
                if eq_assert:
                    logs.info(f"相等断言成功: 接口实际结果 【{new_actual_result}】 == 预期结果: 【{expected_result}】")
                    allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(new_actual_result)}", "相等断言结果: 成功",
                                  attachment_type=allure.attachment_type.JSON)
                else:
                    failure_count += 1
                    logs.error(f"相等断言失败: 接口实际结果 【{new_actual_result}】 != 预期结果: 【{expected_result}】")
                    allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(new_actual_result)}", "相等断言结果: 失败",
                                  attachment_type=allure.attachment_type.JSON)
            else:
                failure_count += 1
                logs.error('相等断言失败, 请检查yaml文件eq模式的预期结果或接口返回值是否正确')

        return failure_count

    @classmethod
    def not_equal_assert(cls, expected_result, response):
        """
        不相等断言, 根据yaml里面的validation关键词下面的ne模式数据去跟接口实际响应信息对比
        :param expected_result: （dict）yaml里面的ne值
        :param response: （dict）接口实际响应结果
        :return:
        """
        failure_count = 0
        # 检查响应和预期结果是否都是字典
        if isinstance(response, dict) and isinstance(expected_result, dict):
            # 找出实际结果与预期结果共同的key值
            common_key = list(expected_result.keys() & response.keys())
            if common_key:
                # 取出第一个共同的键
                common_key = common_key[0]
                # 根据相同的key值去实际结果中获取, 并重新生成一个实际结果的字典
                new_actual_result = {common_key: response[common_key]}
                # 使用operator.ne检查不相等性
                eq_assert = operator.ne(new_actual_result, expected_result)
                if eq_assert:
                    logs.info(f"不相等断言成功: 接口实际结果 【{new_actual_result}】 != 预期结果: 【{expected_result}】")
                    allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(new_actual_result)}",
                                  "不相等断言结果: 成功",
                                  attachment_type=allure.attachment_type.JSON)
                else:
                    failure_count += 1
                    logs.error(f"不相等断言失败: 接口实际结果 【{new_actual_result}】 == 预期结果: 【{expected_result}】")
                    allure.attach(f"预期结果: {str(expected_result)}\n实际结果: {str(new_actual_result)}",
                                  "不相等断言结果: 失败",
                                  attachment_type=allure.attachment_type.JSON)
            else:
                failure_count += 1
                logs.error('不相等断言失败, 请检查yaml文件eq模式的预期结果或接口返回值是否正确')

        return failure_count

    @classmethod
    def database_assert(cls, expected_result, status_code=None):
        """
        数据库断言
        :param expected_result: yaml文件db模式中的SQL语句预期结果
        :param status_code:不做任何操作
        :return:
        """
        failure_count = 0
        conn = ConnectMysql()
        try:
            db_value = conn.query(expected_result)
            if db_value is not None:
                logs.info(f"数据库断言成功: {db_value}")
            else:
                failure_count += 1
                logs.error("数据库断言失败: 请检查数据库中是否存在该数据")
            return failure_count
        except Exception as e:
            failure_count += 1
            logs.error(f"数据库断言失败: {e}")
        return failure_count

    @classmethod
    def assert_result(cls, expected_result, response, status_code):
        """
        断言主函数, 通过all_flag标记, 如all_flag == 0表示测试成功, 否则为失败
        :param expected_result: （list）yaml文件validation关键词下面的预期结果
        :param response: （dict）接口的实际响应信息
        :param status_code: （int）接口的实际响应状态码
        :return:
        """
        all_flag = 0
        # 通过字典映射方式管理不同的断言方式
        assert_methods = {
            'code': cls.status_code_assert,
            'contain': cls.contain_assert,
            'eq': cls.equal_assert,
            'ne': cls.not_equal_assert,
            'db': cls.database_assert
        }

        try:
            # 遍历预期结果列表
            for yq in expected_result:
                # 遍历每个预期结果字典中的键值对
                for assert_mode, assert_value in yq.items():
                    # 获取对应的断言方法。表示assert_method是一个接受两个参数, 类型为Any表示可以是任意类型, 并返回整数的可调用对象,
                    assert_method: Callable[[Any, Any], int] = assert_methods.get(assert_mode)
                    if assert_method:
                        # 如果断言模式是'code'或'db',根据断言方式调用对应的断言方法, 传入相应的参数, 并获取返回值flag
                        if assert_mode in ['code', 'db']:
                            flag = assert_method(assert_value, status_code)
                        # 调用其他断言方法
                        else:
                            flag = assert_method(assert_value, response)
                        # 累加flag到all_flag中
                        all_flag += flag
                    else:
                        logs.error(f'未找到断言方式: {assert_mode}')

        except Exception as e:
            logs.error(f'断言失败: {e}')
            raise

        assert all_flag == 0, '测试失败'  # 断言all_flag是否为0, 如果不为0则抛出测试失败异常
        logs.info('测试成功')
        logs.info('---------------------------------接口测试结束---------------------------------')
